package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;


import lombok.extern.slf4j.Slf4j;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;


import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;


@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    /**
     * 将任意java对象序列化为json并储存在string类型的key中,并且可以设置TTL过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value , Long time , TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(value),time,unit);
    }



    /**
     *缓存穿透解决方案(返回空值)
     * @param keyPrefix     //redis中储存商品信息缓存的key
     * @param id            //要查询的商品的id
     * @param type          //查询到数据后返回的数据类型
     * @param dbFallback    //缓存查不到后,在数据库要执行的方法,这里是执行查询方法,参数传递this::getById,其中this指的是调用本类的方法的对象,如ShopService
     * @param time          //设置存储的空值清除时间 TTL
     * @param unit          //设置清除时间的单位
     * @return
     * @param <R>
     * @param <ID>
     */
    //缓存穿透解决方案(返回空值)
    //如果穿透到查到数据库发现没有要的数据,则把空值""写入缓存,如果有则照常,同时如果在缓存能查到东西还要判断是不是查到空值了
    //如果查到""返回错误信息null,否则照常返回数据
    public <R,ID>  R queryWithPassThrough(String keyPrefix , ID id , Class<R> type , Function<ID,R> dbFallback , Long time , TimeUnit unit) {
        String key = keyPrefix + id;
        //从redis中查寻商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        if (StrUtil.isNotBlank(json)) {
            //存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        //判断命中的是否为空值
        if (json != null) {
            //返回错误信息
            return null;
        }
        //不存在,去数据库查询
        R r = dbFallback.apply(id);

        if (r == null) {
            //如果数据库不存在数据
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", SystemConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return null;
        }

        //如果数据库存在数据,添加写入redis缓存,并返回
        this.set(key , r , time , unit);
        return r;
    }


    /**
     * 缓存击穿解决方案(互斥锁)
     * @param keyPrefix
     * @param keyLock
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @return
     * @param <R>
     * @param <ID>
     */
    //缓存击穿解决方案(互斥锁)
    public <R,ID> R queryWitMutex( String keyPrefix ,String keyLock, ID id , Class<R> type , Function<ID,R> dbFallback , Long time , TimeUnit unit) {
        String key = keyPrefix + id;
        //从redis中查寻商铺缓存
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        if (StrUtil.isNotBlank(jsonStr)) {
            //存在,直接返回
            return JSONUtil.toBean(jsonStr, type);
        }
        //判断命中的是否为空值
        if (jsonStr != null) {
            //返回错误信息
            return null;
        }
        //不存在,实现缓存重建

        //获取互斥锁
        String  lockkey = keyLock + id;
        R r = null;
        try {
            //设置互斥锁
            Boolean isLock = tryLock(lockkey);

            //判断是否获取成功
            if (!isLock){
                //如果失败.则休眠,并重试
                Thread.sleep(50);
                return queryWitMutex(keyPrefix,keyLock,id,type,dbFallback,time,unit);
            }

            //成功,去数据库查询
            r = dbFallback.apply(id);
            //Thread.sleep(200);
            if (r == null) {
                //如果数据库不存在数据
                //将空值写入redis
                this.set(key , "" , 2L , unit);
                return null;
            }

            //如果数据库存在数据,添加写入redis缓存,并返回
            this.set(key, JSONUtil.toJsonStr(r), time + RandomUtil.randomInt(5, 20), unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        finally {
            //释放互斥锁
            unlock(lockkey);
        }
        //返回
        return r;
    }

    /**
     * 缓存击穿解决方案(逻辑过期)
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @return
     * @param <R>
     * @param <ID>
     */
    //缓存击穿解决方案(逻辑过期)
    //(不考虑缓存穿透,因为逻辑过期是用于处理热点key数据的,这类数据通常会在事先主动将数据库中对应的所有数据缓存到redis中)
    //(因此如果查询到不存在缓存redis中的数据时,则数据库中也不存在相应的数据)
    //此时会将一个空的shop和逻辑过期时间组成的RedisData存入缓存,并且永久市场,防止被穿透.类似于生成了一个null在redis中
    public <R,ID> R queryWithLogicalExpire(String keyPrefix , ID id , Class<R> type , Function<ID,R> dbFallback , Long time , TimeUnit unit) {
        String key = keyPrefix + id;

        //从redis中查寻商铺缓存
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        //判断缓存是否命中
        if (StrUtil.isBlank(jsonStr)) {
            //未命中,返回空
            return null;
        }

        //命中,需要把json反序列化为RedisData对象
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
        R json = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        //判断缓存是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            //如果没有过期,返回商铺信息
            return json;
        }

        //判断是否获得互斥锁互斥锁
        String lockKey = SystemConstants.LOCK_SHOP_KEY +id;
        //如果已经过期,尝试获取互斥锁(setnx实现)
        Boolean lock = tryLock(SystemConstants.LOCK_SHOP_KEY);
        if (lock) {
            /**             第一次double chack
             *
             * 当有一个线程获取锁成功后,其余的所有线程都是认为此时的缓存是过期的
             * 也就是说,如果没有第一个double chack 那么除了if (lock) {}里面的线程以外,其余代码中的线程皆认为当前缓存已经过期,需要重建
             * 而如果此时释放锁,在if (lock) {}上面的代码中(此时依旧认为缓存过期的)的线程就会获取锁成功,然后再次执行重建缓存代码,这一次的重建缓存是没有必要的
             *
             * 如果有第一次double chack 在if (lock) {}里面面的代码中(认为缓存过期的)的线程会进行缓存查询,二次判断是否缓存过期
             * 因为有可能现在能拿到锁是因为别人 已经更新完缓存  后 释放锁才让你拿到的,现在缓存都更新了,你却拿到了锁要去再更新一遍,就会造成资源浪费
             */
            //注意:获取锁成功后应该再次检测redis缓存是否过期,做DoubleCheck
            // DoubleCheck
            String checkJsonStr = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(checkJsonStr)) { // 如果存在则无需重建缓存
                RedisData checkRedisData = JSONUtil.toBean(checkJsonStr, RedisData.class);
                LocalDateTime checkExpireTime = checkRedisData.getExpireTime();
                if (checkExpireTime.isAfter(LocalDateTime.now())) {
                    // 当前缓存数据未过期，直接返回
                    return JSONUtil.toBean((JSONObject) checkRedisData.getData(), type);
                }
            }
            CACHE_REBUILD_EXECUTOR.submit(()->{

                //重建缓存
                try {
                    //1.查询店铺数据
                    R r = dbFallback.apply(id);
                    //写入redis
                    this.setWithLogicalExpire(key,r,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                finally {
                    //存入redis后释放锁
                    unlock(lockKey);}
            });
        }
        /**                              第二次double check
         *
         * 如果有第二次double check此时如果释放锁,那么上面的代码中(认为缓存过期的)的线程会查询到刚刚更新的最新缓存,而不会依旧返回旧数据,使数据一致性增强
         * 并且这里的double check在上面抢锁失败的线程,不走更新缓存,而是直接再做一次查询缓存(乐观锁),这样当抢锁成功的线程在更新缓存时,没抢到锁的线程也可以返回
         * 说不定缓存数据已经被抢锁成功的线程更新了,并且这些抢不到锁的线程也不会阻塞在这里,而是直接把旧数据返回,只是会让数据的实时性一般
         */
        //DoubleChack
        jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(jsonStr)) {
            //未命中,返回空
            return null;
        }
        // 3.2 缓存命中，将JSON字符串反序列化未对象，并判断缓存数据是否逻辑过期
        redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
        json = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 当前缓存数据未过期，直接返回
            return json;
        }

        //返回旧数据
        return json;
    }

    /**
     * 将任意java对象序列化为json并储存在string类型的key中,并且可以设置逻辑过期时间,用于处理 缓存击穿问题
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key, Object value , Long time , TimeUnit unit) {
        //设置逻辑过期
        RedisData redisData = RedisData.builder()
                .data(value)
                .expireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)))
                .build();

        //写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    /**
     * 设置互斥锁(用redis的setnx实现)
     * @param key
     * @return
     */
    //设置互斥锁(用redis的setnx实现)
    private Boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //这里不要直接返回flag,直接返回可能会出现空指针
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    //释放锁
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

}
